// Copyright 2000-2006, FreeHEP
package org.freehep.graphicsio;

import java.awt.*;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.font.TextLayout;
import java.awt.geom.*;
import java.awt.image.*;
import java.awt.image.renderable.RenderContext;
import java.awt.image.renderable.RenderableImage;
import java.io.IOException;
import java.text.AttributedCharacterIterator;
import java.util.Arrays;
import java.util.Map;

import org.freehep.graphics2d.font.FontEncoder;
import org.freehep.util.images.ImageUtilities;

/**
 * This class provides an abstract VectorGraphicsIO class for specific output
 * drivers.
 * 
 * @author Charles Loomis
 * @author Mark Donszelmann
 * @author Steffen Greiffenberg
 * @version $Id: AbstractVectorGraphicsIO.java,v 1.6 2009/08/17 21:44:45 murkle
 *          Exp $
 */
public abstract class AbstractVectorGraphicsIO extends VectorGraphicsIO {

  private static final String rootKey = AbstractVectorGraphicsIO.class
      .getName();

  public static final String EMIT_WARNINGS = rootKey + ".EMIT_WARNINGS";

  public static final String TEXT_AS_SHAPES = rootKey + ".TEXT_AS_SHAPES";

  public static final String EMIT_ERRORS = rootKey + ".EMIT_ERRORS";

  public static final String CLIP = rootKey + ".CLIP";

  /*
   * ============================================================================
   * ==== Table of Contents: ------------------ 1. Constructors & Factory
   * Methods 2. Document Settings 3. Header, Trailer, Multipage & Comments 3.1
   * Header & Trailer 3.2 MultipageDocument methods 4. Create & Dispose 5.
   * Drawing Methods 5.1. shapes (draw/fill) 5.1.1. lines, rectangles, round
   * rectangles 5.1.2. polylines, polygons 5.1.3. ovals, arcs 5.1.4. shapes 5.2.
   * Images 5.3. Strings 6. Transformations 7. Clipping 8. Graphics State /
   * Settings 8.1. stroke/linewidth 8.2. paint/color 8.3. font 8.4. rendering
   * hints 9. Auxiliary 10. Private/Utility Methods
   * ==============================
   * ==================================================
   */

  private final Dimension size;

  private final Component component;

  private boolean doRestoreOnDispose;

  private Rectangle deviceClip;

  /**
   * Untransformed clipping Area defined by the user
   */
  private Area userClip;

  private final AffineTransform currentTransform;

  // only for use in writeSetTransform to calculate the difference.
  private final AffineTransform oldTransform = new AffineTransform();

  private Composite currentComposite;

  private Stroke currentStroke;

  private final RenderingHints hints;

  /*
   * ============================================================================
   * ==== 1. Constructors & Factory Methods
   * ======================================
   * ==========================================
   */

  /**
   * Constructs a subgraphics context.
   * 
   * @param graphics
   *          context to clone from
   * @param doRestoreOnDispose
   *          true if writeGraphicsRestore() should be called when this graphics
   *          context is disposed of.
   */
  protected AbstractVectorGraphicsIO(AbstractVectorGraphicsIO graphics,
      boolean doRestoreOnDispose) {
    super(graphics);
    this.doRestoreOnDispose = doRestoreOnDispose;

    size = new Dimension(graphics.size);
    component = graphics.component;

    deviceClip = new Rectangle(graphics.deviceClip);
    userClip = graphics.userClip != null ? new Area(graphics.userClip) : null;
    currentTransform = new AffineTransform(graphics.currentTransform);
    currentComposite = graphics.currentComposite;
    currentStroke = graphics.currentStroke;
    hints = graphics.hints;
  }

  /**
   * Constructs a Graphics context with the following graphics state:
   * <UL>
   * <LI>Paint: The color of the component.
   * <LI>Font: The font of the component.
   * <LI>Stroke: Linewidth 1.0; No Dashing; Miter Join Style; Miter Limit 10;
   * Square Endcaps;
   * <LI>Transform: The getDefaultTransform for the GraphicsConfiguration of the
   * component.
   * <LI>Composite: AlphaComposite.SRC_OVER
   * <LI>Clip: The size of the component, Rectangle(0, 0, size.width,
   * size.height)
   * </UL>
   * 
   * @param component
   *          to be used to initialize the values of the graphics state
   * @param doRestoreOnDispose
   *          true if writeGraphicsRestore() should be called when this graphics
   *          context is disposed of.
   */
  protected AbstractVectorGraphicsIO(Component component,
      boolean doRestoreOnDispose) {
    super();

    size = component.getSize();
    this.component = component;
    this.doRestoreOnDispose = doRestoreOnDispose;

    deviceClip = size != null
        ? new Rectangle(0, 0, size.width, size.height)
        : null;
    userClip = null;
    GraphicsConfiguration gc = component.getGraphicsConfiguration();
    currentTransform = gc != null
        ? gc.getDefaultTransform()
        : new AffineTransform();
    currentComposite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER);
    currentStroke = new BasicStroke(1.0f, BasicStroke.CAP_SQUARE,
        BasicStroke.JOIN_MITER, 10.0f, null, 0.0f);

    super.setFont(component.getFont());
    super.setBackground(component.getBackground());
    super.setColor(component.getForeground());

    // Initialize the rendering hints.
    hints = new RenderingHints(null);
  }

  /**
   * Constructs a Graphics context with the following graphics state:
   * <UL>
   * <LI>Paint: black
   * <LI>Font: Dailog, Plain, 12pt
   * <LI>Stroke: Linewidth 1.0; No Dashing; Miter Join Style; Miter Limit 10;
   * Square Endcaps;
   * <LI>Transform: Identity
   * <LI>Composite: AlphaComposite.SRC_OVER
   * <LI>Clip: Rectangle(0, 0, size.width, size.height)
   * </UL>
   * 
   * @param size
   *          rectangle specifying the bounds of the image
   * @param doRestoreOnDispose
   *          true if writeGraphicsRestore() should be called when this graphics
   *          context is disposed of.
   */
  protected AbstractVectorGraphicsIO(Dimension size, boolean doRestoreOnDispose) {
    super();

    this.size = size;
    component = null;
    this.doRestoreOnDispose = doRestoreOnDispose;

    deviceClip = size != null
        ? new Rectangle(0, 0, size.width, size.height)
        : null;
    userClip = null;
    currentTransform = new AffineTransform();
    currentComposite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER);
    currentStroke = new BasicStroke(1.0f, BasicStroke.CAP_SQUARE,
        BasicStroke.JOIN_MITER, 10.0f, null, 0.0f);

    super.setColor(Color.BLACK);
    super.setBackground(Color.BLACK);
    super.setFont(new Font("Dialog", Font.PLAIN, 12));

    // Initialize the rendering hints.
    hints = new RenderingHints(null);
  }

  /**
   * Adds to table of rendering hints.
   * 
   * @param hints
   *          table to be added
   */
  @Override
  public void addRenderingHints(Map hints) {
    hints.putAll(hints);
  }

  /**
   * Clears rectangle by painting it with the backgroundColor.
   * 
   * @param x
   *          rectangle to be cleared.
   * @param y
   *          rectangle to be cleared.
   * @param width
   *          rectangle to be cleared.
   * @param height
   *          rectangle to be cleared.
   */
  @Override
  public void clearRect(double x, double y, double width, double height) {
    Paint temp = getPaint();
    setPaint(getBackground());
    fillRect(x, y, width, height);
    setPaint(temp);
  }

  /**
   * Clips using given shape. Dispatches to writeClip(Rectangle),
   * writeClip(Rectangle2D) or writeClip(Shape).
   * 
   * @param s
   *          used for clipping
   */
  @Override
  public void clip(Shape s) {
    Shape ts = transformShape(s);
    if (userClip != null) {
      if (ts != null)
        userClip.intersect(new Area(ts));
      else
        userClip = null;
    } else
      userClip = ts != null ? new Area(ts) : null;

    try {
      writeClip(s);
    } catch (IOException e) {
      handleException(e);
    }
  }

  /**
   * Clips rectangle. Calls clip(Rectangle2D).
   * 
   * @param x
   *          rectangle for clipping
   * @param y
   *          rectangle for clipping
   * @param width
   *          rectangle for clipping
   * @param height
   *          rectangle for clipping
   */
  @Override
  public void clipRect(double x, double y, double width, double height) {
    clip(new Rectangle2D.Double(x, y, width, height));
  }

  /**
   * Clips rectangle. Calls clip(Rectangle).
   * 
   * @param x
   *          rectangle for clipping
   * @param y
   *          rectangle for clipping
   * @param width
   *          rectangle for clipping
   * @param height
   *          rectangle for clipping
   */
  @Override
  public void clipRect(int x, int y, int width, int height) {
    clip(new Rectangle(x, y, width, height));
  }

  /**
   * Called to close the stream you are writing to.
   */
  public abstract void closeStream() throws IOException;

  @Override
  protected Shape createShape(double[] xPoints, double[] yPoints, int nPoints,
      boolean close) {
    GeneralPath path = new GeneralPath(GeneralPath.WIND_EVEN_ODD);
    if (nPoints > 0) {
      path.moveTo((float) xPoints[0], (float) yPoints[0]);
      for (int i = 1; i < nPoints; i++)
        path.lineTo((float) xPoints[i], (float) yPoints[i]);
      if (close)
        path.closePath();
    }
    return path;
  }

  /*
   * ============================================================================
   * ==== 4. Create & Dispose
   * ====================================================
   * ============================
   */
  /**
   * Disposes of the graphics context. If on creation restoreOnDispose was true,
   * writeGraphicsRestore() will be called.
   */
  @Override
  public void dispose() {
    try {
      // Swing sometimes calls dispose several times for a given
      // graphics object. Ensure that the grestore is only written
      // once if this happens.
      if (doRestoreOnDispose) {
        writeGraphicsRestore();
        doRestoreOnDispose = false;
      }
    } catch (IOException e) {
      handleException(e);
    }
  }

  /**
   * Use the transformation of the glyphvector and draw it
   * 
   * @param g
   * @param x
   * @param y
   */
  @Override
  public void drawGlyphVector(GlyphVector g, float x, float y) {
    fill(g.getOutline(x, y));
  }

  @Override
  public void drawImage(BufferedImage img, BufferedImageOp op, int x, int y) {
    drawImage(op.filter(img, null), x, y, null);
  }

  @Override
  public boolean drawImage(Image image, AffineTransform xform,
      ImageObserver observer) {
    drawRenderedImage(
        ImageUtilities.createRenderedImage(image, observer, null), xform);
    return true;
  }

  @Override
  public boolean drawImage(Image image, int x, int y, Color bgColor,
      ImageObserver observer) {
    int imageWidth = image.getWidth(observer);
    int imageHeight = image.getHeight(observer);
    return drawImage(image, x, y, x + imageWidth, y + imageHeight, 0, 0,
        imageWidth, imageHeight, bgColor, observer);
  }

  /* 5.3. Images */
  @Override
  public boolean drawImage(Image image, int x, int y, ImageObserver observer) {
    int imageWidth = image.getWidth(observer);
    int imageHeight = image.getHeight(observer);
    return drawImage(image, x, y, x + imageWidth, y + imageHeight, 0, 0,
        imageWidth, imageHeight, null, observer);
  }

  @Override
  public boolean drawImage(Image image, int x, int y, int width, int height,
      Color bgColor, ImageObserver observer) {
    int imageWidth = image.getWidth(observer);
    int imageHeight = image.getHeight(observer);
    return drawImage(image, x, y, x + width, y + height, 0, 0, imageWidth,
        imageHeight, bgColor, observer);
  }

  @Override
  public boolean drawImage(Image image, int x, int y, int width, int height,
      ImageObserver observer) {
    int imageWidth = image.getWidth(observer);
    int imageHeight = image.getHeight(observer);
    return drawImage(image, x, y, x + width, y + height, 0, 0, imageWidth,
        imageHeight, null, observer);
  }

  /*
   * ============================================================================
   * ==== | 5. Drawing Methods
   * ==================================================
   * ==============================
   */

  /**
   * Draw and resizes (transparent) image. Calls writeImage(...).
   * 
   * @param image
   *          image to be drawn
   * @param dx1
   *          destination image bounds
   * @param dy1
   *          destination image bounds
   * @param dx2
   *          destination image bounds
   * @param dy2
   *          destination image bounds
   * @param sx1
   *          source image bounds
   * @param sy1
   *          source image bounds
   * @param sx2
   *          source image bounds
   * @param sy2
   *          source image bounds
   * @param bgColor
   *          background color
   * @param observer
   *          for updates if image still incomplete
   * @return true if successful
   */
  @Override
  public boolean drawImage(Image image, int dx1, int dy1, int dx2, int dy2,
      int sx1, int sy1, int sx2, int sy2, Color bgColor, ImageObserver observer) {
    try {
      int srcX = Math.min(sx1, sx2);
      int srcY = Math.min(sy1, sy2);
      int srcWidth = Math.abs(sx2 - sx1);
      int srcHeight = Math.abs(sy2 - sy1);
      int width = Math.abs(dx2 - dx1);
      int height = Math.abs(dy2 - dy1);

      if (srcX != 0 || srcY != 0 || srcWidth != image.getWidth(observer)
          || srcHeight != image.getHeight(observer)) {
        // crop the source image
        ImageFilter crop = new CropImageFilter(srcX, srcY, srcWidth, srcHeight);
        image = Toolkit.getDefaultToolkit().createImage(
            new FilteredImageSource(image.getSource(), crop));
        MediaTracker mediaTracker = new MediaTracker(new Panel());
        mediaTracker.addImage(image, 0);
        try {
          mediaTracker.waitForAll();
        } catch (InterruptedException e) {
          handleException(e);
        }
      }

      boolean flipHorizontal = dx2 < dx1 ^ sx2 < sx1; // src flipped
      // and not dest
      // flipped or
      // vice versa
      boolean flipVertical = dy2 < dy1 ^ sy2 < sy1; // <=> source
      // flipped XOR
      // dest flipped

      double tx = flipHorizontal ? (double) dx2 : (double) dx1;
      double ty = flipVertical ? (double) dy2 : (double) dy1;

      double sx = (double) width / srcWidth;
      sx = flipHorizontal ? -1 * sx : sx;
      double sy = (double) height / srcHeight;
      sy = flipVertical ? -1 * sy : sy;

      writeImage(ImageUtilities.createRenderedImage(image, observer, bgColor),
          new AffineTransform(sx, 0, 0, sy, tx, ty), bgColor);
      return true;
    } catch (IOException e) {
      handleException(e);
      return false;
    }
  }

  @Override
  public boolean drawImage(Image image, int dx1, int dy1, int dx2, int dy2,
      int sx1, int sy1, int sx2, int sy2, ImageObserver observer) {
    return drawImage(image, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, null,
        observer);
  }

  // NOTE: not tested yet!!!
  @Override
  public void drawRenderableImage(RenderableImage image, AffineTransform xform) {
    drawRenderedImage(image.createRendering(new RenderContext(
        new AffineTransform(), getRenderingHints())), xform);
  }

  /*
   * // first use the original orientation int clippingWidth = Math.abs(sx2 -
   * sx1); int clippingHeight = Math.abs(sy2 - sy1); int sulX = Math.min(sx1,
   * sx2); int sulY = Math.min(sy1, sy2); Image background = null; if (bgColor
   * != null) { // Draw the image on the background color // maybe we could crop
   * it and fill the transparent pixels in one go // by means of a filter.
   * background = new BufferedImage(clippingWidth, clippingHeight,
   * BufferedImage.TYPE_INT_ARGB); Graphics bgGfx = background.getGraphics();
   * bgGfx.drawImage(image, 0, 0, clippingWidth, clippingWidth, sulX, sulY,
   * sulX+clippingWidth, sulY+clippingHeight, getPrintColor(bgColor), observer);
   * } else { // crop the source image ImageFilter crop = new
   * CropImageFilter(sulX, sulY, clippingWidth, clippingHeight); background =
   * Toolkit.getDefaultToolkit().createImage(new
   * FilteredImageSource(image.getSource(), crop)); MediaTracker mediaTracker =
   * new MediaTracker(new Panel()); mediaTracker.addImage(background, 0); try {
   * mediaTracker.waitForAll(); } catch (InterruptedException e) {
   * handleException(e); } } // now flip the image if necessary boolean
   * flipHorizontal = (dx2<dx1) ^ (sx2<sx1); // src flipped and not dest flipped
   * or vice versa boolean flipVertical = (dy2<dy1) ^ (sy2<sy1); // <=> source
   * flipped XOR dest flipped int destWidth = Math.abs(dx2-dx1); int destHeight
   * = Math.abs(dy2-dy1); try { return writeImage(background, flipHorizontal ?
   * dx2 : dx1, flipVertical ? dy2 : dy1, flipHorizontal ? -destWidth :
   * destWidth, flipVertical ? -destHeight : destHeight, (bgColor == null),
   * observer); } catch (IOException e) { return false; } }
   */
  /**
   * Draws a rendered image using a transform.
   * 
   * @param image
   *          to be drawn
   * @param xform
   *          transform to be used on the image
   */
  @Override
  public void drawRenderedImage(RenderedImage image, AffineTransform xform) {
    try {
      writeImage(image, xform, null);
    } catch (Exception e) {
      handleException(e);
    }
  }

  @Override
  public void drawString(AttributedCharacterIterator iterator, float x, float y) {

    // TextLayout draws the iterator as glyph vector
    // thats why we use it only in the case of TEXT_AS_SHAPES,
    // otherwise tagged strings are always written as glyphs
    if (isProperty(TEXT_AS_SHAPES)) {
      // draws all attributes
      TextLayout tl = new TextLayout(iterator, getFontRenderContext());
      tl.draw(this, x, y);
    } else {
      // reset to that font at the end
      Font font = getFont();

      // initial attributes, we us TextAttribute.equals() rather
      // than Font.equals() because using Font.equals() we do
      // not get a 'false' if underline etc. is changed
      Map attributes = font.getAttributes();

      // stores all characters which are written with the same font
      // if font is changed the buffer will be written and cleared
      // after it
      StringBuffer sb = new StringBuffer();

      for (char c = iterator.first(); c != AttributedCharacterIterator.DONE; c = iterator
          .next())
        // append c if font is not changed
        if (attributes.equals(iterator.getAttributes()))
          sb.append(c);
        else {
          // draw sb if font is changed
          drawString(sb.toString(), x, y);

          // change the x offset for the next drawing
          // FIXME: change y offset for vertical text
          TextLayout tl = new TextLayout(sb.toString(), attributes,
              getFontRenderContext());

          // calculate real width
          x = x + Math.max(tl.getAdvance(), (float) tl.getBounds().getWidth());

          // empty sb
          sb = new StringBuffer();
          sb.append(c);

          // change the font
          attributes = iterator.getAttributes();
          setFont(new Font(attributes));
        }

      // draw the rest
      if (sb.length() > 0)
        drawString(sb.toString(), x, y);

      // use the old font for the next string drawing
      setFont(font);
    }
  }

  /**
   * Draws the string at (x, y). If TEXT_AS_SHAPES is set
   * {@link #drawGlyphVector(java.awt.font.GlyphVector, float, float)} is used,
   * otherwise {@link #writeString(String, double, double)} for a more direct
   * output of the string.
   * 
   * @param string
   * @param x
   * @param y
   */
  @Override
  public void drawString(String string, double x, double y) {
    // something to draw?
    if (string == null || string.equals(""))
      return;

    // draw strings directly?
    if (isProperty(TEXT_AS_SHAPES)) {

      Font font = getFont();

      // NOTE, see FVG-199, createGlyphVector does not seem to create the proper
      // glyphcodes
      // for either ZapfDingbats or Symbol. We use our own encoding which seems
      // to work...
      String fontName = font.getName();
      if (fontName.equals("Symbol") || fontName.equals("ZapfDingbats")) {
        string = FontEncoder.getEncodedString(string, fontName);
        // use a standard font, not Symbol.
        font = new Font("Serif", font.getStyle(), font.getSize());
      }

      // create glyph
      GlyphVector gv = font.createGlyphVector(getFontRenderContext(), string);

      // draw it
      drawGlyphVector(gv, (float) x, (float) y);
    } else
      // write string directly
      try {
        writeString(string, x, y);
      } catch (IOException e) {
        handleException(e);
      }
  }

  @Override
  public void endExport() {
    try {
      dispose();
      writeTrailer();
      closeStream();
    } catch (IOException e) {
      handleException(e);
    }
  }

  /**
   * Gets the current clip in form of a Shape (Rectangle).
   * 
   * @return current clip
   */
  @Override
  public Shape getClip() {
    return userClip != null ? new Area(untransformShape(userClip)) : null;
  }

  /**
   * Gets the current clip in form of a Rectangle.
   * 
   * @return current clip
   */
  @Override
  public Rectangle getClipBounds() {
    Shape clip = getClip();
    return clip != null ? getClip().getBounds() : null;
  }

  /**
   * Gets the current clip in form of a Rectangle.
   * 
   * @return current clip
   */
  @Override
  public Rectangle getClipBounds(Rectangle r) {
    Rectangle bounds = getClipBounds();
    if (bounds != null)
      r.setBounds(bounds);
    return r;
  }

  public Component getComponent() {
    return component;
  }

  /*
   * ============================================================================
   * ==== | 9. Auxiliary
   * ========================================================
   * ========================
   */
  /**
   * Gets current composite.
   * 
   * @return current composite
   */
  @Override
  public Composite getComposite() {
    return currentComposite;
  }

  /**
   * Gets the fontmetrics.
   * 
   * @deprecated
   * @param font
   *          to be used for retrieving fontmetrics
   * @return fontmetrics for given font
   */
  @Deprecated
  @Override
  public FontMetrics getFontMetrics(Font font) {
    return Toolkit.getDefaultToolkit().getFontMetrics(font);
  }

  /* 8.3. font */
  /**
   * Gets the current font render context. This returns an standard
   * FontRenderContext with an upside down matrix, anti-aliasing and uses
   * fractional metrics.
   * 
   * @return current font render context
   */
  @Override
  public FontRenderContext getFontRenderContext() {
    // NOTE: not sure?
    return new FontRenderContext(new AffineTransform(1, 0, 0, -1, 0, 0), true,
        true);
  }

  /**
   * Gets a given rendering hint.
   * 
   * @param key
   *          hint key
   * @return hint associated to key
   */
  @Override
  public Object getRenderingHint(RenderingHints.Key key) {
    return hints.get(key);
  }

  /* 8.4. rendering hints */
  /**
   * Gets a copy of the rendering hints.
   * 
   * @return clone of table of rendering hints.
   */
  @Override
  public RenderingHints getRenderingHints() {
    return (RenderingHints) hints.clone();
  }

  /*
   * ============================================================================
   * ==== | 2. Document Settings
   * ================================================
   * ================================
   */
  @Override
  public Dimension getSize() {
    return size;
  }

  /*
   * ============================================================================
   * ==== | 8. Graphics State
   * ====================================================
   * ============================
   */
  /* 8.1. stroke/linewidth */
  /**
   * Get the current stroke.
   * 
   * @return current stroke
   */
  @Override
  public Stroke getStroke() {
    return currentStroke;
  }

  /*
   * ============================================================================
   * ==== | 6. Transformations
   * ==================================================
   * ==============================
   */
  /**
   * Get the current transform.
   * 
   * @return current transform
   */
  @Override
  public AffineTransform getTransform() {
    return new AffineTransform(currentTransform);
  }

  /**
   * Handles an exception which has been caught. Dispatches exception to
   * writeWarning for UnsupportedOperationExceptions and writeError for others
   * 
   * @param exception
   *          to be handled
   */
  protected void handleException(Exception exception) {
    if (exception instanceof UnsupportedOperationException)
      writeWarning(exception);
    else
      writeError(exception);
  }

  /**
   * Draws an overline for the text at (x, y). The method is usesefull for
   * drivers that do not support overlines by itself.
   * 
   * @param text
   *          text for width calulation
   * @param font
   *          font for width calulation
   * @param x
   *          position of text
   * @param y
   *          position of text
   */
  protected void overLine(String text, Font font, float x, float y) {
    TextLayout layout = new TextLayout(text, font, getFontRenderContext());
    float width = Math.max(layout.getAdvance(), (float) layout.getBounds()
        .getWidth());

    GeneralPath path = new GeneralPath();
    path.moveTo(x, y + (float) layout.getBounds().getY() - layout.getAscent());
    path.lineTo(x + width, y + (float) layout.getBounds().getY()
        - layout.getAscent() - layout.getAscent());
    draw(path);
  }

  @Override
  public void printComment(String comment) {
    try {
      writeComment(comment);
    } catch (IOException e) {
      handleException(e);
    }
  }

  /* 3.2 MultipageDocument methods */
  protected void resetClip(Rectangle clip) {
    deviceClip = clip;
    userClip = null;
  }

  /**
   * Rotate the current transform over the Z-axis. Calls
   * writeTransform(Transform). Rotating with a positive angle theta rotates
   * points on the positive x axis toward the positive y axis.
   * 
   * @param theta
   *          radians over which to rotate
   */
  @Override
  public void rotate(double theta) {
    currentTransform.rotate(theta);
    try {
      writeTransform(new AffineTransform(Math.cos(theta), Math.sin(theta),
          -Math.sin(theta), Math.cos(theta), 0, 0));
    } catch (IOException e) {
      handleException(e);
    }
  }

  /**
   * Scales the current transform. Calls writeTransform(Transform).
   * 
   * @param sx
   *          amount used for scaling
   * @param sy
   *          amount used for scaling
   */
  @Override
  public void scale(double sx, double sy) {
    currentTransform.scale(sx, sy);
    try {
      writeTransform(new AffineTransform(sx, 0, 0, sy, 0, 0));
    } catch (IOException e) {
      handleException(e);
    }
  }

  /*
   * ============================================================================
   * ==== | 7. Clipping
   * ==========================================================
   * ======================
   */

  /**
   * Clips rectangle. Calls clip(Rectangle2D).
   * 
   * @param x
   *          rectangle for clipping
   * @param y
   *          rectangle for clipping
   * @param width
   *          rectangle for clipping
   * @param height
   *          rectangle for clipping
   */
  @Override
  public void setClip(double x, double y, double width, double height) {
    setClip(new Rectangle2D.Double(x, y, width, height));
  }

  /**
   * Clips rectangle. Calls clip(Rectangle).
   * 
   * @param x
   *          rectangle for clipping
   * @param y
   *          rectangle for clipping
   * @param width
   *          rectangle for clipping
   * @param height
   *          rectangle for clipping
   */
  @Override
  public void setClip(int x, int y, int width, int height) {
    setClip(new Rectangle(x, y, width, height));
  }

  /**
   * Clips shape. Clears userClip and calls clip(Shape).
   * 
   * @param s
   *          used for clipping
   */
  @Override
  public void setClip(Shape s) {

    Shape ts = transformShape(s);
    userClip = ts != null ? new Area(ts) : null;

    try {
      writeSetClip(s);
    } catch (IOException e) {
      handleException(e);
    }
  }

  /* 8.2 Paint */
  @Override
  public void setColor(Color color) {
    if (color == null)
      return;

    if (color.equals(getColor()))
      return;

    try {
      super.setColor(color);
      writePaint(getPrintColor(color));
    } catch (IOException e) {
      handleException(e);
    }
  }

  /**
   * Sets current composite.
   * 
   * @param composite
   *          to be set
   */
  @Override
  public void setComposite(Composite composite) {
    currentComposite = composite;
  }

  /**
   * Sets the current font.
   * 
   * @param font
   *          to be set
   */
  @Override
  public void setFont(Font font) {
    if (font == null)
      return;

    // FIXME: maybe add delayed setting
    super.setFont(font);

    // write the font
    try {
      writeFont(font);
    } catch (IOException e) {
      handleException(e);
    }
  }

  /**
   * Sets the current paint. Dispatches to writePaint(Color),
   * writePaint(GradientPaint), writePaint(TexturePaint paint) or
   * writePaint(Paint). In the case paint is a Color the current color is also
   * changed.
   * 
   * @param paint
   *          to be set
   */
  @Override
  public void setPaint(Paint paint) {
    if (paint == null)
      return;

    if (paint.equals(getPaint()))
      return;

    try {
      if (paint instanceof Color)
        setColor((Color) paint);
      else if (paint instanceof GradientPaint) {
        super.setPaint(paint);
        writePaint((GradientPaint) paint);
      } else if (paint instanceof TexturePaint) {
        super.setPaint(paint);
        writePaint((TexturePaint) paint);
      } else {
        super.setPaint(paint);
        writePaint(paint);
      }
    } catch (IOException e) {
      handleException(e);
    }
  }

  /**
   * Sets a given rendering hint.
   * 
   * @param key
   *          hint key
   * @param hint
   *          to be associated with key
   */
  @Override
  public void setRenderingHint(RenderingHints.Key key, Object hint) {
    // extra protection, failed on under MacOS X 10.2.6, jdk 1.4.1_01-39/14
    if (key == null || hint == null)
      return;
    hints.put(key, hint);
  }

  /**
   * Sets table of rendering hints.
   * 
   * @param hints
   *          table to be set
   */
  @Override
  public void setRenderingHints(Map hints) {
    hints.clear();
    hints.putAll(hints);
  }

  /**
   * Sets the current stroke. Calls writeStroke if stroke is unequal to the
   * current stroke.
   * 
   * @param stroke
   *          to be set
   */
  @Override
  public void setStroke(Stroke stroke) {
    if (stroke.equals(currentStroke))
      return;
    try {
      writeStroke(stroke);
    } catch (IOException e) {
      handleException(e);
    }
    currentStroke = stroke;
  }

  /**
   * Set the current transform. Calls writeSetTransform(Transform).
   * 
   * @param transform
   *          to be set
   */
  @Override
  public void setTransform(AffineTransform transform) {
    // Fix for FREEHEP-569
    oldTransform.setTransform(currentTransform);
    currentTransform.setTransform(transform);
    try {
      writeSetTransform(transform);
    } catch (IOException e) {
      handleException(e);
    }
  }

  /**
   * Shears the current transform. Calls writeTransform(Transform).
   * 
   * @param shx
   *          amount for shearing
   * @param shy
   *          amount for shearing
   */
  @Override
  public void shear(double shx, double shy) {
    currentTransform.shear(shx, shy);
    try {
      writeTransform(new AffineTransform(1, shy, shx, 1, 0, 0));
    } catch (IOException e) {
      handleException(e);
    }
  }

  /*
   * ============================================================================
   * ==== | 3. Header, Trailer, Multipage & Comments
   * ============================
   * ====================================================
   */
  /* 3.1 Header & Trailer */
  @Override
  public void startExport() {
    try {
      writeHeader();

      // delegate this to openPage if it is a MultiPage document
      if (!(this instanceof MultiPageDocument)) {
        writeGraphicsState();
        writeBackground();
      }
    } catch (IOException e) {
      handleException(e);
    }
  }

  /**
   * Transforms the current transform. Calls writeTransform(Transform)
   * 
   * @param transform
   *          to be applied
   */
  @Override
  public void transform(AffineTransform transform) {
    currentTransform.concatenate(transform);
    try {
      writeTransform(transform);
    } catch (IOException e) {
      handleException(e);
    }
  }

  private Shape transformShape(AffineTransform at, Shape s) {
    if (s == null)
      return null;
    return at.createTransformedShape(s);
  }

  private Shape transformShape(Shape s) {
    return transformShape(getTransform(), s);
  }

  /**
   * Translates the current transform. Calls writeTransform(Transform)
   * 
   * @param x
   *          amount by which to translate
   * @param y
   *          amount by which to translate
   */
  @Override
  public void translate(double x, double y) {
    currentTransform.translate(x, y);
    try {
      writeTransform(new AffineTransform(1, 0, 0, 1, x, y));
    } catch (IOException e) {
      handleException(e);
    }
  }

  private Shape untransformShape(Shape s) {
    if (s == null)
      return null;
    try {
      return transformShape(getTransform().createInverse(), s);
    } catch (NoninvertibleTransformException e) {
      return null;
    }
  }

  public abstract void writeBackground() throws IOException;

  /**
   * Writes out the cap of the stroke.
   * 
   * @param cap
   *          of the stroke
   */
  protected void writeCap(int cap) throws IOException {
    writeWarning(getClass() + ": writeCap() not implemented.");
  }

  /**
   * Write out Shape clip.
   * 
   * @param shape
   *          to be used for clipping
   */
  protected abstract void writeClip(Shape shape) throws IOException;

  /**
   * Called to Write out a comment.
   * 
   * @param comment
   *          to be written
   */
  public abstract void writeComment(String comment) throws IOException;

  /**
   * Writes out the dash of the stroke.
   * 
   * @deprecated use writeDash(float[], float)
   * @param dash
   *          dash pattern, empty array is solid line
   * @param phase
   *          of the dash pattern
   */
  @Deprecated
  protected void writeDash(double[] dash, double phase) throws IOException {
    writeWarning(getClass() + ": writeDash() not implemented.");
  }

  /**
   * Writes out the dash of the stroke.
   * 
   * @param dash
   *          dash pattern, empty array is solid line
   * @param phase
   *          of the dash pattern
   */
  protected void writeDash(float[] dash, float phase) throws IOException {
    // for backward compatibility
    double[] dd = new double[dash.length];
    for (int i = 0; i < dash.length; i++)
      dd[i] = dash[i];
    writeDash(dd, phase);
  }

  /**
   * Writes out an error, by default the stack trace is printed.
   * 
   * @param exception
   *          error to be reported
   */
  protected void writeError(Exception exception) {
    throw new RuntimeException(exception);
    // FIXME decide what we should do
    /*
     * if (isProperty(EMIT_ERRORS)) { System.err.println(exception);
     * exception.printStackTrace(System.err); }
     */
  }

  /**
   * Writes the font
   * 
   * @param font
   *          to be written
   */
  protected abstract void writeFont(Font font) throws IOException;

  /**
   * Writes out the restore of a graphics context. Some implementations keep
   * track of this by hand if the output format does not support it.
   */
  protected abstract void writeGraphicsRestore() throws IOException;

  /**
   * Writes out the save of a graphics context for a later restore. Some
   * implementations keep track of this by hand if the output format does not
   * support it.
   */
  protected abstract void writeGraphicsSave() throws IOException;

  /**
   * Called to write the initial graphics state.
   */
  public void writeGraphicsState() throws IOException {
    writePaint(getPrintColor(getColor()));

    writeSetTransform(getTransform());

    // writeStroke(getStroke());

    setClip(getClip());

    // Silly assignment, Font is written when String is drawed and "extra"
    // writeFont does not exist
    // setFont(getFont());

    // Silly assignment and "extra" writeComposite does not exist
    // setComposite(getComposite);
  }

  /**
   * Called to write the header part of the output.
   */
  public abstract void writeHeader() throws IOException;

  protected abstract void writeImage(RenderedImage image,
      AffineTransform xform, Color bkg) throws IOException;

  /**
   * Writes out the join of the stroke.
   * 
   * @param join
   *          of the stroke
   */
  protected void writeJoin(int join) throws IOException {
    writeWarning(getClass() + ": writeJoin() not implemented.");
  }

  /**
   * Writes out the miter limit of the stroke.
   * 
   * @param limit
   *          miter limit of the stroke
   */
  protected void writeMiterLimit(float limit) throws IOException {
    writeWarning(getClass() + ": writeMiterLimit() not implemented.");
  }

  /**
   * Writes out paint as the given color.
   * 
   * @param color
   *          to be written
   */
  protected abstract void writePaint(Color color) throws IOException;

  /**
   * Writes out paint as the given gradient.
   * 
   * @param paint
   *          to be written
   */
  protected abstract void writePaint(GradientPaint paint) throws IOException;

  /**
   * Writes out paint.
   * 
   * @param paint
   *          to be written
   */
  protected abstract void writePaint(Paint paint) throws IOException;

  /**
   * Writes out paint as the given texture.
   * 
   * @param paint
   *          to be written
   */
  protected abstract void writePaint(TexturePaint paint) throws IOException;

  /**
   * Write out Shape clip.
   * 
   * @param shape
   *          to be used for clipping
   */
  protected abstract void writeSetClip(Shape shape) throws IOException;

  /**
   * Clears any existing transformation and sets the a new one. The default
   * implementation calls writeTransform using the inverted affine transform to
   * calculate it. s *
   * 
   * @param transform
   *          to be written
   */
  protected void writeSetTransform(AffineTransform transform)
      throws IOException {
    try {
      AffineTransform deltaTransform = new AffineTransform(transform);
      transform.concatenate(oldTransform.createInverse());
      writeTransform(deltaTransform);
    } catch (NoninvertibleTransformException e) {
      // ignored...
    }
  }

  protected abstract void writeString(String string, double x, double y)
      throws IOException;

  /**
   * Writes the current stroke. If stroke is an instance of BasicStroke it will
   * call writeWidth, writeCap, writeJoin, writeMiterLimit and writeDash, if any
   * were different than the current stroke.
   */
  protected void writeStroke(Stroke stroke) throws IOException {
    if (stroke instanceof BasicStroke) {
      BasicStroke ns = (BasicStroke) stroke;

      // get the current values for comparison if available,
      // otherwise set them to -1="undefined"
      int currentCap = -1, currentJoin = -1;
      float currentWidth = -1, currentLimit = -1, currentDashPhase = -1;
      float[] currentDashArray = null;
      if (currentStroke != null && currentStroke instanceof BasicStroke) {
        BasicStroke cs = (BasicStroke) currentStroke;
        currentCap = cs.getEndCap();
        currentJoin = cs.getLineJoin();
        currentWidth = cs.getLineWidth();
        currentLimit = cs.getMiterLimit();
        currentDashArray = cs.getDashArray();
        currentDashPhase = cs.getDashPhase();
      }

      // Check the linewidth.
      float width = ns.getLineWidth();
      if (currentWidth != width)
        writeWidth(width);

      // Check the line caps.
      int cap = ns.getEndCap();
      if (currentCap != cap)
        writeCap(cap);

      // Check the line joins.
      int join = ns.getLineJoin();
      if (currentJoin != join)
        writeJoin(join);

      // Check the miter limit and validity of value
      float limit = ns.getMiterLimit();
      if (currentLimit != limit && limit >= 1.0f)
        writeMiterLimit(limit);

      // Check to see if there are differences in the phase or dash
      if (!Arrays.equals(currentDashArray, ns.getDashArray())
          || currentDashPhase != ns.getDashPhase())
        // write the dashing parameters
        if (ns.getDashArray() != null)
          writeDash(ns.getDashArray(), ns.getDashPhase());
        else
          writeDash(new float[0], ns.getDashPhase());
    }
  }

  /**
   * Called to write the trailing part of the output.
   */
  public abstract void writeTrailer() throws IOException;

  /**
   * Writes out the transform as it needs to be concatenated to the internal
   * transform of the output format. If there is no implementation of an
   * internal transform, then this method needs to do nothing, BUT all
   * coordinates need to be transformed by the currentTransform before being
   * written out.
   * 
   * @param transform
   *          to be written
   */
  protected abstract void writeTransform(AffineTransform transform)
      throws IOException;

  /**
   * Writes out a warning, by default to System.err.
   * 
   * @param exception
   *          warning to be written
   */
  private void writeWarning(Exception exception) {
    writeWarning(exception.getMessage());
  }

  /**
   * Writes out a warning, by default to System.err.
   * 
   * @param warning
   *          to be written
   */
  protected void writeWarning(String warning) {
    if (isProperty(EMIT_WARNINGS))
      System.err.println(warning);
  }

  /**
   * Writes out the width of the stroke.
   * 
   * @param width
   *          of the stroke
   */
  protected void writeWidth(float width) throws IOException {
    writeWarning(getClass() + ": writeWidth() not implemented.");
  }
}
